/*
 * Copyright 2010, 2011, 2012, 2013 mapsforge.org
 * Copyright 2014 Ludwig M Brinckmann
 *
 * This program is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package tau_sadna.tau_map;


import org.mapsforge.core.graphics.Bitmap;
import org.mapsforge.core.graphics.Canvas;
import org.mapsforge.core.graphics.GraphicFactory;
import org.mapsforge.core.graphics.Paint;
import org.mapsforge.core.graphics.Style;
import org.mapsforge.core.model.BoundingBox;
import org.mapsforge.core.model.LatLong;
import org.mapsforge.core.model.Point;
import org.mapsforge.map.android.graphics.AndroidGraphicFactory;
import org.mapsforge.map.layer.Layer;
import org.mapsforge.map.layer.overlay.Circle;
import org.mapsforge.map.layer.overlay.Marker;
import org.mapsforge.map.model.MapViewPosition;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Bundle;
import android.widget.Toast;

/**
 * A thread-safe {@link Layer} implementation to display the current location. NOTE: This code really does not reflect
 * Android best practice and used in production leads to bad user experience (e.g. long time to first fix, excessive
 * battery use, non-compliance with the Android lifecycle...). Best use the new location services provided by Google
 * Play Services. Also note that MyLocationOverlay needs to be added to a view before requesting location updates
 * (otherwise no DisplayModel is set).
 */
public class CompassGPS extends Layer implements LocationListener,SensorEventListener {
	private static final GraphicFactory GRAPHIC_FACTORY = AndroidGraphicFactory.INSTANCE;
	private static final int UPDATE_DISTANCE = 0;
	private static final int UPDATE_INTERVAL = 1000;
	
	private static final int WORKING_FULL = 0;
	private static final int WORKING_HALF = 1;
	private static final int NOT_WORKING = 2;

	/**
	 * @param location
	 *            the location whose geographical coordinates should be converted.
	 * @return a new LatLong with the geographical coordinates taken from the given location.
	 */
	public static LatLong locationToLatLong(Location location) {
		return new LatLong(location.getLatitude(), location.getLongitude());
	}

	private static Paint getDefaultCircleFill() {
		return getPaint(GRAPHIC_FACTORY.createColor(48, 0, 0, 255), 0, Style.FILL);
	}

	private static Paint getDefaultCircleStroke() {
		return getPaint(GRAPHIC_FACTORY.createColor(160, 0, 0, 255), 2, Style.STROKE);
	}

	private static Paint getPaint(int color, int strokeWidth, Style style) {
		Paint paint = GRAPHIC_FACTORY.createPaint();
		paint.setColor(color);
		paint.setStrokeWidth(strokeWidth);
		paint.setStyle(style);
		return paint;
	}

	private boolean centerAtNextFix;
	private final Circle circle;
	private Location lastLocation;
	private final LocationManager locationManager;
	private final MapViewPosition mapViewPosition;
	private final Marker marker;
	private boolean myLocationEnabled;
	private boolean snapToLocationEnabled;
	private Bitmap bitmap = null;
	private Context context;
	protected float radius;
	
	private boolean gpsEnable;
	private boolean gpsWorking;
	private long lastGpsTime;
	private static final long TIME_NOT_WORK = 60 * 1000000000; //60 seconds.

	/**
	 * Constructs a new {@code MyLocationOverlay} with the default circle paints.
	 * 
	 * @param context
	 *            a reference to the application context.
	 * @param mapViewPosition
	 *            the {@code MapViewPosition} whose location will be updated.
	 * @param bitmap
	 *            a bitmap to display at the current location (might be null).
	 */
	public CompassGPS(Context context, MapViewPosition mapViewPosition, Bitmap bitmap) {
		this(context, mapViewPosition, bitmap, getDefaultCircleFill(), getDefaultCircleStroke());
	}

	/**
	 * Constructs a new {@code MyLocationOverlay} with the given circle paints.
	 * 
	 * @param context
	 *            a reference to the application context.
	 * @param mapViewPosition
	 *            the {@code MapViewPosition} whose location will be updated.
	 * @param bitmap
	 *            a bitmap to display at the current location (might be null).
	 * @param circleFill
	 *            the {@code Paint} used to fill the circle that represents the current location (might be null).
	 * @param circleStroke
	 *            the {@code Paint} used to stroke the circle that represents the current location (might be null).
	 */
	public CompassGPS(Context context, MapViewPosition mapViewPosition, Bitmap bitmap, Paint circleFill,
			Paint circleStroke) {
		super();
			
		this.mapViewPosition = mapViewPosition;
		this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
		
		this.bitmap = Utils.rotateBitmap(bitmap, 0);
		this.bitmap.incrementRefCount(); //Important

		this.marker = new Marker(null, bitmap, 0, 0);
		this.circle = new Circle(null, 0, circleFill, circleStroke);
		
		
		this.context = context;
		this.lastLocation = null;
		this.myLocationEnabled = false;
		this.gpsWorking = false;
		this.gpsEnable = false;
		
		initCompass();
		
		setState(NOT_WORKING);
	}

	
	/**
	 * Stops the receiving of location updates. Has no effect if location updates are already disabled.
	 */
	public synchronized void disableMyLocation() {
		if (this.myLocationEnabled) {
			this.myLocationEnabled = false;
			this.lastLocation = null;
			this.gpsWorking = false;
			this.gpsEnable = false;
			this.locationManager.removeUpdates(this);
			this.sensorManager.unregisterListener(this, sensorAccelerometer);
			setState(NOT_WORKING);
			// TODO trigger redraw?
		}
	}

	@Override
	public synchronized void draw(BoundingBox boundingBox, byte zoomLevel, Canvas canvas, Point topLeftPoint) {
		if (!this.myLocationEnabled || lastLocation == null) {
			return;
		}

		this.circle.draw(boundingBox, zoomLevel, canvas, topLeftPoint);
		this.marker.draw(boundingBox, zoomLevel, canvas, topLeftPoint);
	}

	/**
	 * Enables the receiving of location updates from the most accurate {@link LocationProvider} available.
	 * 
	 * @param centerAtFirstFix
	 *            whether the map should be centered to the first received location fix.
	 * @return true if at least one location provider was found, false otherwise.
	 */
	public synchronized boolean enableMyLocation(boolean centerAtFirstFix) {
		disableMyLocation(); // assert that disable.
		boolean gpsSeccses = enableGpsProvider();
		boolean networkSeccses = enableNetworkProvider();
		if (!gpsSeccses && !networkSeccses) {
			return false;
		}	
		enableSensors();
		
		setState(WORKING_HALF);

		this.centerAtNextFix = centerAtFirstFix;
		this.circle.setDisplayModel(this.displayModel);
		this.marker.setDisplayModel(this.displayModel);
		return true;
	}

	/**
	 * @return the most-recently received location fix (might be null).
	 */
	public synchronized Location getLastLocation() {
		return this.lastLocation;
	}

	/**
	 * @return true if the map will be centered at the next received location fix, false otherwise.
	 */
	public synchronized boolean isCenterAtNextFix() {
		return this.centerAtNextFix;
	}

	/**
	 * @return true if the receiving of location updates is currently enabled, false otherwise.
	 */
	public synchronized boolean isMyLocationEnabled() {
		return this.myLocationEnabled;
	}

	/**
	 * @return true if the snap-to-location mode is enabled, false otherwise.
	 */
	public synchronized boolean isSnapToLocationEnabled() {
		return this.snapToLocationEnabled;
	}

	@Override
	public void onDestroy() {
		this.marker.onDestroy();
	}
	
	public synchronized void checkGpsWorking(){
		if(!gpsWorking || !myLocationEnabled)
			return;
		
		if(System.nanoTime() - lastGpsTime >= TIME_NOT_WORK){
			gpsWorking = false;
			setState(WORKING_HALF);
		}
	}

	@Override
	public void onLocationChanged(Location location) {
		synchronized (this) {
			if(location.getProvider().equals(LocationManager.GPS_PROVIDER)){
				if(gpsWorking){
					lastGpsTime = System.nanoTime();
				} else {
					gpsWorking = true;
					setState(WORKING_FULL);
				}
			} else if(location.getProvider().equals(LocationManager.NETWORK_PROVIDER) && gpsWorking){
				return;
			}
			
			this.lastLocation = location;

			LatLong latLong = locationToLatLong(location);
			this.marker.setLatLong(latLong);
			this.circle.setLatLong(latLong);
			if (location.getAccuracy() != 0) {
				radius = location.getAccuracy();
			} else {
				// on the emulator we do not get an accuracy
				radius = 40;
			}
			this.circle.setRadius(radius);

			if (this.centerAtNextFix || this.snapToLocationEnabled) {
				this.centerAtNextFix = false;
				centerAt();
			}

			requestRedraw();
		}
	}

	@Override
	public void onProviderDisabled(String provider) {
		synchronized (this){
			if(isMyLocationEnabled()){
				enableMyLocation(centerAtNextFix);
				return;
			}
		}
	}

	@Override
	public void onProviderEnabled(String provider) {
		synchronized (this){
			if(isMyLocationEnabled()){
				enableMyLocation(centerAtNextFix);
				return;
			}
		}
	}

	@Override
	public void onStatusChanged(String provider, int status, Bundle extras) {
		// do nothing
	}

	/**
	 * @param snapToLocationEnabled
	 *            whether the map should be centered at each received location fix.
	 */
	public synchronized void setSnapToLocationEnabled(boolean snapToLocationEnabled) {
		this.snapToLocationEnabled = snapToLocationEnabled;
	}

	 /**
     * enable GPS.
     * @return true if could enable GPS and false otherwise.
     */
	private synchronized boolean enableGpsProvider() {
            // check if GPS is working.
            if(!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)){
            	Toast.makeText(context, "No GPS", Toast.LENGTH_LONG).show();
            	return false;
            }
            
            // make the GPS work better.
            Bundle bundle = new Bundle();
            locationManager.sendExtraCommand(LocationManager.GPS_PROVIDER,"force_time_injection",bundle);
            locationManager.sendExtraCommand(LocationManager.GPS_PROVIDER,"force_xtra_injection",bundle);
            
            // enable GPS.
            this.locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, UPDATE_INTERVAL, UPDATE_DISTANCE, this);
            this.myLocationEnabled = true;
            this.gpsEnable = true;
            
            return true;
    }
	
	private synchronized void enableSensors(){
		this.sensorManager.registerListener(this, sensorAccelerometer, SensorManager.SENSOR_DELAY_NORMAL);
        this.sensorManager.registerListener(this, sensorMagneticField, SensorManager.SENSOR_DELAY_NORMAL);
	}
	
	 /**
     * enable NETWORK Provider.
     * @return true if could enable GPS and false otherwise.
     */
	private synchronized boolean enableNetworkProvider() {
            // check if NETWORK_PROVIDER is working.
            if(!locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)){
            	return false;
            }
            
            
            // enable NETWORK_PROVIDER.
            this.locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, UPDATE_INTERVAL, UPDATE_DISTANCE, this);
            this.myLocationEnabled = true;
            
            return true;
    }
	
	SensorManager sensorManager;
	private Sensor sensorAccelerometer;
	private Sensor sensorMagneticField;
	  
	private float[] valuesAccelerometer;
	private float[] valuesMagneticField;
	   
	private float[] matrixR;
	private float[] matrixI;
	private float[] matrixValues;
	 
	 private void initCompass() {
			sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
			sensorAccelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
			sensorMagneticField = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
		      
			valuesAccelerometer = new float[3];
			valuesMagneticField = new float[3];
		  
			matrixR = new float[9];
			matrixI = new float[9];
			matrixValues = new float[3];
		}

	
	@Override
	public void onAccuracyChanged(Sensor arg0, int arg1) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void onSensorChanged(SensorEvent event) {
		switch(event.sensor.getType()){
			case Sensor.TYPE_ACCELEROMETER:
				for(int i =0; i < 3; i++){
					valuesAccelerometer[i] = event.values[i];
				}
				break;
			case Sensor.TYPE_MAGNETIC_FIELD:
				for(int i =0; i < 3; i++){
					valuesMagneticField[i] = event.values[i];
				}
				break;
		}
		    
		boolean success = SensorManager.getRotationMatrix(matrixR, matrixI, valuesAccelerometer, valuesMagneticField);

		if(success && bitmap != null){
			SensorManager.getOrientation(matrixR, matrixValues);

			Bitmap rotatedBitmap = Utils.rotateBitmap(bitmap, (float) Math.toDegrees(matrixValues[0]));
			rotatedBitmap.incrementRefCount();
			marker.setBitmap(rotatedBitmap);
		}
	}
	

	
	@Override
	public LatLong getPosition() {
		if(lastLocation == null)
			return null;
		return new LatLong(lastLocation.getLatitude(), lastLocation.getLongitude());
	}

	public void centerAt() {
		if(lastLocation == null)
			return;
		LatLong latLong = locationToLatLong(lastLocation);
		if(mapViewPosition.getMapLimit().contains(latLong)){
			//only if contains.
			this.mapViewPosition.setCenter(latLong);
		}		
	}	
	
	private void setState(int state) {
		switch (state) {
		case WORKING_FULL:
			((MainActivity)(context)).setGpsIconState(GpsIcon.STATE_ON);
			break;
		case WORKING_HALF:
			if(gpsEnable)
				((MainActivity)(context)).setGpsIconState(GpsIcon.STATE_PENDING);
			else
				((MainActivity)(context)).setGpsIconState(GpsIcon.STATE_OFF);
			break;	
		case NOT_WORKING:
		default:
			((MainActivity)(context)).setGpsIconState(GpsIcon.STATE_OFF);
			break;
		}

	}
}
